﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Xml.Serialization;
using MediaSage.Lib.Database.Entities.Client;
using MediaSage.Lib.UIFramework.DataFile;
using MediaSage.Lib.UIFramework.Dialog.Entities;
using MediaSage.Lib.Utilities;
using NHibernate.Criterion;

namespace MediaSage.Lib.UIFramework.Theme
{
    /// <summary>
    /// When UIServer is initiated this will be initiated and load all the object's themes once.
    /// This was when a new object is instantiated, it will only have to look up its XAML (Vs reload it from the disk for each object)
    ///
    /// The ThemeManager will be called to do all UI.  It will be responsible for:
    ///   1. Drawing the actual UI from the loaded theme
    ///   2. Falling back to the default UI for items that the loaded theme doesn't have
    ///
    /// This also is where anything that wants to draw any UI calls to draw said UI.  The Theme Manager will provider either the loaded theme's UI primitive or
    /// the defaults if the loaded theme doesn't have that defined.
    /// </summary>
    public class ThemeManager
    {
        protected Client.Config.Configuration config;
        protected Logger logger;
        private string themeDir;
        private ThemeObject defaultTheme;
        private ThemeObject loadedTheme;

        public ThemeObject DefaultTheme
        {
            get { return defaultTheme; }
        }

        public ThemeObject LoadedTheme
        {
            get { return loadedTheme; }
        }

        /// <summary>
        /// Dictionary of loaded themes (So we don't have to reload them in the future
        /// name, ThemeObject
        /// </summary>
        private IDictionary<string, ThemeObject> themes;

        private readonly IDictionary<string, AppDomain> domains;

        public ThemeManager(Client.Config.Configuration configuration)
        {
            this.config = configuration;
            logger.Log("UIFramework.ThemeManager initialized with default theme");
            this.themeDir = config.ThemeDirectory;
            initializeThemes();
        }

        public string ThemeDirectory
        {
            get { return themeDir; }
        }

        public bool SetLoadedTheme(string name)
        {
            logger.Log(String.Format("Entering SetLoadedTheme({0})", name));
            if (themes.ContainsKey(name))
            {
                loadedTheme = themes[name];
                logger.Log(String.Format("Exiting SetLoadedTheme({0}), rc==true", name));
                return true;
            }

            IDictionary<ThemeData, bool> allThemes = scanThemeXMLFiles();
            bool found = false;
            foreach (KeyValuePair<ThemeData, bool> td in allThemes)
            {
                if (td.Key.Name.Equals(name))
                {
                    if (td.Value == false)
                    {
                        // We expected it to not be loaded yet, so load it
                        ThemeObject to = LoadTheme(td.Key.DLL);
                        if (to != null)
                        {
                            themes.Add(to.ThemeName, to);
                            found = true;
                        }
                        else
                        {
                            logger.Log(String.Format("Failed to load the theme: {0}", td.Key.Name));
                            logger.Log(String.Format("Exiting SetLoadedTheme({0}), rc==false", name));
                            return false;
                        }
                    }
                }
            }
            if (found == false)
            {
                logger.Log("Theme not found, try installing it");
                logger.Log(String.Format("Exiting SetLoadedTheme({0}), rc==false", name));
                return false;
            }
            logger.Log(String.Format("Exiting SetLoadedTheme({0}), rc==true", name));
            return true;
        }

        /// <summary>
        /// Loads all of the themes in the theme directory
        /// </summary>
        /// <returns>Dictionary containing a ThemeData and a bool (Signifies if its loaded already)</returns>
        private IDictionary<ThemeData, bool> scanThemeXMLFiles()
        {
            logger.Log("Entering scanThemeXMLFiles()");
            if (!Directory.Exists(themeDir))
            {
                logger.Log(Logger.Levels.ERROR, String.Format("Theme directory: {0} does not exist, reinstall MediaSage"));
                return null;
            }

            var returnVal = new Dictionary<ThemeData, bool>();
            string[] filePaths = Directory.GetFiles(themeDir, "THEME.XML", SearchOption.AllDirectories);
            foreach (string themeFile in filePaths)
            {
                ThemeData td = getThemeData(Path.GetDirectoryName(themeFile));
                if (td != null)
                {
                    bool loaded = themes.ContainsKey(td.Name);
                    if (!returnVal.ContainsKey(td))
                    {
                        logger.Log(Logger.Levels.WARN,
                                   String.Format(
                                       "Duplicate entries for theme, second theme is in: {0}, theme's name: {1}",
                                       Path.GetDirectoryName(themeFile), td.Name));
                    }
                    else
                    {
                        returnVal.Add(td, loaded);
                    }
                }
            }
            logger.Log(String.Format("Exiting scanThemeXMLFiles() with: {0} entries", returnVal.Count));
            return returnVal;
        }

        private ThemeData getThemeData(string themeDataDir)
        {
            logger.Log(String.Format("Entering MediaSage.MCClient.Code.Themes.getThemeData({0})", themeDataDir));
            string themeXMLFile = Path.Combine(themeDataDir, "THEME.XML");
            if (!File.Exists(themeXMLFile))
            {
                logger.Log(String.Format("File: {0} does not exist", themeXMLFile));
                logger.Log(String.Format("Exiting MediaSage.MCClient.Code.Themes.getThemeData({0}), rc==null",
                                         themeDataDir));
                return null;
            }

            ThemeData data = null;
            try
            {
                XmlSerializer xmlSerializer = new XmlSerializer(typeof(ThemeData));
                TextReader tr = new StreamReader(themeXMLFile);
                data = (ThemeData)xmlSerializer.Deserialize(tr);
                tr.Close();
            }
            catch (Exception e)
            {
                logger.Log(String.Format("Exception deserializing file: {0}.  Message: {1}", themeXMLFile, e.Message));
                logger.Log(String.Format("Exiting MediaSage.MCClient.Code.Themes.getThemeData({0}), rc==null",
                                         themeDataDir));
                return null;
            }

            logger.Log(String.Format("Exiting MediaSage.MCClient.Code.Themes.getThemeData({0}), rc==ThemeData structure",
                                     themeDataDir));
            return data;
        }

        private void initializeThemes()
        {
            logger.Log("Entering initializeThemes()");
            string themeDir = Path.Combine(config.BaseDirectory, "Themes");
            if (!Directory.Exists(themeDir))
            {
                // This is a big issue, we cant really draw anything without our themes
                // TODO: Include the default as code eventually
                logger.Log(Logger.Levels.ERROR, String.Format("Theme Dir: {0} does not exist, try reinstalling", themeDir));
                return;
            }

            // Each directory will have an XML File (Theme.XML) that has
            // <THEME DLL="foo.dll" MAJOR="1" MINOR="0" BUILD="10" NAME="ThemeName" DEFAULT="true">
            // <CAPABILITY>PageName</CAPABILITY>
            // Where:
            //      DLL         - The DLL that contains the theme object
            //      MAJOR       - Major version number of the theme
            //      MINOR       - Minor version of the theme
            //      BUILD       - Build number of the theme
            //      NAME        - The name of the theme as shown to the user
            //      DEFAULT     - Boolean to define if this is the default theme (Only one of these)
            //      CAPABILITY  - A list of all the Default pages that will be overridden by this theme
            //                    any pages that aren't in Default and in the theme do NOT need to be defined

            themes = new Dictionary<string, ThemeObject>();
            string[] filePaths = Directory.GetFiles(themeDir, "THEME.XML", SearchOption.AllDirectories);
            foreach (string themeFile in filePaths)
            {
                ThemeData td = getThemeData(Path.GetDirectoryName(themeFile));
                ThemeObject to = LoadTheme(Path.GetDirectoryName(themeFile));
                if (td.Default)
                {
                    defaultTheme = to;
                }
                ThemeEntity te = getThemeEntityFromDB(td.Name);
                to.ThemeDBEntity = te;
                if (te.IsActive)
                {
                    foreach (KeyValuePair<string, ThemeObject> kvp in themes)
                    {
                        if (kvp.Key.ToUpper().Equals(te.Name.ToUpper()))
                        {
                            loadedTheme = kvp.Value;
                            break;
                        }
                    }
                }
                themes.Add(td.Name,to);
            }
            logger.Log("Exiting initializeThemes()");

        }

        /// <summary>
        /// Used to load themes from disk
        /// </summary>
        /// <param name="themeLocation"></param>
        /// <returns>A theme object</returns>
        public ThemeObject LoadTheme(string themeLocation)
        {
            logger.Log(String.Format("Entering GetTheme({0})", themeLocation));
            if (themes.ContainsKey(themeLocation))
            {
                logger.Log("Theme already loaded");
                logger.Log(String.Format("Exiting GetTheme({0})", themeLocation));
                return (themes[themeLocation]);
            }

            if (!domains.ContainsKey(themeLocation))
            {
                var ads = new AppDomainSetup
                {
                    ApplicationBase = Path.GetDirectoryName(themeLocation),
                    DisallowBindingRedirects = false,
                    DisallowCodeDownload = true
                };

                domains.Add(themeLocation, AppDomain.CreateDomain(themeLocation, null, ads));
            }

            var loader =
                (RemoteLoader)
                domains[themeLocation].CreateInstanceFromAndUnwrap("MediaSage", "MediaSage.MCClient.Code.Themes.RemoteLoader");

            Assembly theme = null;
            try
            {
                theme = loader.Load(Path.GetFileNameWithoutExtension(themeLocation));
            }
            catch (Exception e)
            {
                logger.Log("Exception loading theme: " + e.Message);
                logger.Log("Exiting GetTheme(" + themeLocation + "), rc==null");
                return null;
            }
            ThemeObject b = null;
            foreach (Type type in theme.GetTypes())
            {
                if (type.IsSubclassOf(typeof(ThemeObject)) && type.IsAbstract == false)
                {
                    try
                    {
                        b = (ThemeObject)Activator.CreateInstance(type, config);
                    }
                    catch (Exception e)
                    {
                        logger.Log(Logger.Levels.ERROR, String.Format("Exception loading theme: {0}, Message: {1}", themeLocation, e.Message));
                        logger.Log("Exiting GetTheme(" + themeLocation + "), rc==null");
                        return null;
                    }
                }
            }
            if (b != null)
            {
                themes.Add(themeLocation, b);
            }

            logger.Log("Exiting GetTheme(" + themeLocation + "), theme loaded successfully");
            return b;
        }

        private bool isFileLocked(IOException exception)
        {
            var errorCode = Marshal.GetHRForException(exception) & ((1 << 16) - 1);
            return errorCode == 32 || errorCode == 33;
        }

        public void UnloadTheme(string themeLocation)
        {
            logger.Log("Entering UnloadTheme(" + themeLocation + ")");
            if (themes.ContainsKey(themeLocation))
            {
                if (domains.ContainsKey(themeLocation))
                {
                    AppDomain.Unload(domains[themeLocation]);
                    themes.Remove(themeLocation);
                    logger.Log(String.Format("Unloaded theme @{0}", themeLocation));

                    // Check to see if the file is locked, if it is, sleep a bit and try up to 4 times
                    var attempts = 0;
                    bool lockFree = false;
                    while (true)
                    {
                        try
                        {
                            var test = File.Open(themeLocation, FileMode.Open, FileAccess.Read, FileShare.None);
                            if (test != null)
                            {
                                lockFree = true;
                                test.Close();
                                logger.Log(String.Format("No lock on {0}", themeLocation));
                                logger.Log("Exiting UnloadTheme(" + themeLocation + ")");
                                return;
                            }
                        }
                        catch (IOException e)
                        {
                            if (!isFileLocked(e))
                            {
                                logger.Log(Logger.Levels.ERROR,
                                           String.Format("Exception unlocking {0}, msg: {1}", themeLocation, e.Message));
                                logger.Log("Exiting UnloadPlugin(" + themeLocation + ")");
                                return;
                            }
                            if (++attempts > 10)
                            {
                                logger.Log(Logger.Levels.ERROR,
                                           String.Format("Couldnt unlock {0}, reload may fail", themeLocation));
                                return;
                            }
                        }
                        catch (Exception e)
                        {
                            logger.Log(Logger.Levels.ERROR,
                                       String.Format("Exception unlocking {0}, msg: {1}", themeLocation, e.Message));
                            logger.Log("Exiting UnloadTheme(" + themeLocation + ")");
                            return;
                        }
                        Thread.Sleep(500);
                    }
                }
            }
            logger.Log("Exiting UnloadTheme(" + themeLocation + ")");
        }

        /// <remarks>
        /// These are our various methods for displaying content
        /// Each of these will either:
        ///     <list type="bullet">
        ///     <item>
        ///         <term>Display theme object</term>
        ///         <description>If a theme has the capability, it will display the theme's object</description>
        ///     </item>
        ///     <item>
        ///         <term>Display default theme's object</term>
        ///         <description>If the theme cannot draw the object, we will fall back to the default theme's object</description>
        ///     </item>
        ///     </list>
        /// </remarks>

        /// <summary>
        /// A modal window with buttons for answers
        /// </summary>
        /// <param name="dialogEntity"></param>
        /// <returns></returns>
        public bool DialogWindow(DialogMappingEntity dialogEntity)
        {
            if (loadedTheme.ThemeDBEntity.Capability.Contains(ThemeCapabilities.DialogWindow))
            {
                loadedTheme.DialogWindow(dialogEntity);                
            }
            else
            {
                defaultTheme.DialogWindow(dialogEntity);
            }
            return true;
        }

        public bool NotificationWindow(DialogMappingEntity dialogEntity)
        {
            return true;
        }

        private ThemeEntity getThemeEntityFromDB(string name)
        {
            logger.Log(String.Format("Entering getThemeEntityFromDB({0})", name));
            var te =
                    DBUtil.GetSession().CreateCriteria<ThemeEntity>().Add(Restrictions.Eq("Name", name)).UniqueResult
                        <ThemeEntity>();
            if (te == null)
            {
                logger.Log(String.Format("Exiting getThemeEntityFromDB({0}), rc==null", name));
                return null;
            }
            logger.Log(String.Format("Exiting getThemeEntityFromDB({0}), rc==ThemeEntity", name));
            return te;
        }
    }

    public class RemoteLoader : MarshalByRefObject
    {
        #region Methods

        public Assembly Load(string themeName)
        {
            return AppDomain.CurrentDomain.Load(themeName);
        }

        #endregion Methods
    }
}